/* ****************************************************************************
importer.cpp
-------------------------------------------------------------------------------

Copyright (c) 2017, Tain L.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

**************************************************************************** */

#include <iomanip>

#pragma comment(lib, "../../../third-parties/assimp-3.3.1-bin/assimp-vc140-mt.lib")

#include "importer.h"

using namespace Spears;

namespace impl
{
	const static unsigned int ai_default_ppsteps =
		aiProcess_CalcTangentSpace | // calculate tangents and bitangents if possible
		aiProcess_JoinIdenticalVertices | // join identical vertices/ optimize indexing
		aiProcess_ValidateDataStructure | // perform a full validation of the loader's output
		aiProcess_ImproveCacheLocality | // improve the cache locality of the output vertices
		aiProcess_RemoveRedundantMaterials | // remove redundant materials
		aiProcess_FindDegenerates | // remove degenerated polygons from the import
		aiProcess_FindInvalidData | // detect invalid model data, such as invalid normal vectors
		aiProcess_GenUVCoords | // convert spherical, cylindrical, box and planar mapping to proper UVs
		aiProcess_TransformUVCoords | // preprocess UV transformations (scaling, translation ...)
		aiProcess_FindInstances | // search for instanced meshes and remove them by references to one master
		aiProcess_LimitBoneWeights | // limit bone weights to 4 per vertex
		aiProcess_OptimizeMeshes | // join small meshes, if possible;
		aiProcess_SplitByBoneCount | // split meshes with too many bones. Necessary for our (limited) hardware skinning shader
		aiProcess_GenSmoothNormals | // generate smooth normal vectors if not existing
		aiProcess_SplitLargeMeshes | // split large, unrenderable meshes into submeshes
		aiProcess_Triangulate | // triangulate polygons with more than 3 edges
		aiProcess_ConvertToLeftHanded | // convert everything to D3D left handed space
		aiProcess_SortByPType | // make 'clean' meshes which consist of a single typ of primitives		
		0;

	void save_color(nlohmann::json& j_color, const Asset::atColor& color);
	void save_material(nlohmann::json& j_material, const Asset::atMaterial& material);
	void save_transform(nlohmann::json& j_transform, const Asset::atTransform& transform);
	void save_tree(nlohmann::json& root, Asset::atNode const* p_root);


	void load_assimp_mesh(Asset::atMesh& dst_mesh, const aiMesh& src_mesh)
	{
		if (src_mesh.HasPositions())
		{
			assert(true == src_mesh.HasNormals());

			Asset::atPoint	point;
			Asset::atNormal normal;

			for (size_t i = 0; i < src_mesh.mNumVertices; ++i)
			{
				point.x = src_mesh.mVertices[i].x;
				point.y = src_mesh.mVertices[i].y;
				point.z = src_mesh.mVertices[i].z;

				normal.x = src_mesh.mNormals[i].x;
				normal.y = src_mesh.mNormals[i].y;
				normal.z = src_mesh.mNormals[i].z;

				dst_mesh.m_vertices.push_back(point);
				dst_mesh.m_normals.push_back(normal);
			}
		}
		else
			return;

		if (src_mesh.HasFaces())
		{
			Asset::atFace face;

			for (size_t i = 0; i < src_mesh.mNumFaces; ++i)
			{
				assert(src_mesh.mFaces[i].mNumIndices == 3);

				face.v1 = src_mesh.mFaces[i].mIndices[0];
				face.v2 = src_mesh.mFaces[i].mIndices[1];
				face.v3 = src_mesh.mFaces[i].mIndices[2];

				dst_mesh.m_faces.push_back(face);
			}
		}
		else
			return;

		dst_mesh.m_material_index = src_mesh.mMaterialIndex;

		//
		// TODO:
		// LOAD MORE MESH DATA

		return;
	}

	void load_assimp_light(Asset::atLight& dst_light, const aiLight& src_light)
	{
		dst_light.m_name = src_light.mName.C_Str();
		switch (src_light.mType)
		{
		case aiLightSourceType::aiLightSource_AMBIENT:
			dst_light.m_type = Asset::atLight::Ambient;
			break;

		case aiLightSourceType::aiLightSource_AREA:
			dst_light.m_type = Asset::atLight::Area;
			break;

		case aiLightSourceType::aiLightSource_DIRECTIONAL:
			dst_light.m_type = Asset::atLight::Directional;
			break;

		case aiLightSourceType::aiLightSource_POINT:
			dst_light.m_type = Asset::atLight::Point;
			break;

		case aiLightSourceType::aiLightSource_SPOT:
			dst_light.m_type = Asset::atLight::Spot;
			break;

		default:
			dst_light.m_type = Asset::atLight::Unknown;
		}

		dst_light.m_ambient.a = 1.0;
		dst_light.m_ambient.r = src_light.mColorAmbient.r;
		dst_light.m_ambient.g = src_light.mColorAmbient.g;
		dst_light.m_ambient.b = src_light.mColorAmbient.b;

		dst_light.m_diffuse.a = 1.0;
		dst_light.m_diffuse.r = src_light.mColorDiffuse.r;
		dst_light.m_diffuse.g = src_light.mColorDiffuse.g;
		dst_light.m_diffuse.b = src_light.mColorDiffuse.b;

		dst_light.m_specular.a = 1.0;
		dst_light.m_specular.r = src_light.mColorSpecular.r;
		dst_light.m_specular.g = src_light.mColorSpecular.g;
		dst_light.m_specular.b = src_light.mColorSpecular.b;

		dst_light.m_angle_inner_cone = src_light.mAngleInnerCone;
		dst_light.m_angle_outer_cone = src_light.mAngleOuterCone;
		dst_light.m_attenuation_constant = src_light.mAttenuationConstant;
		dst_light.m_attenuation_linear = src_light.mAttenuationLinear;
		dst_light.m_attenuation_quadratic = src_light.mAttenuationQuadratic;

		dst_light.m_position.x = src_light.mPosition.x;
		dst_light.m_position.y = src_light.mPosition.y;
		dst_light.m_position.z = src_light.mPosition.z;

		dst_light.m_direction.x = src_light.mDirection.x;
		dst_light.m_direction.y = src_light.mDirection.y;
		dst_light.m_direction.z = src_light.mDirection.z;

		dst_light.m_up.x = src_light.mUp.x;
		dst_light.m_up.y = src_light.mUp.y;
		dst_light.m_up.z = src_light.mUp.z;
	}
	
	void load_assimp_mtrl_phong(Asset::atMaterial& dst_material, const aiMaterial& src_material)
	{
		// try to load the diffuse color of material
		{
			aiColor3D aiColor;
			if (AI_SUCCESS == src_material.Get(AI_MATKEY_COLOR_DIFFUSE, aiColor))
			{
				Asset::atColor clr_diffuse(1.0f, aiColor.r, aiColor.g, aiColor.b);
				dst_material.set(Asset::atMaterial::CLR_DIFFUSE, clr_diffuse);
			}
		}

		// try to load the diffuse color of material
		{
			aiColor3D aiColor;
			if (AI_SUCCESS == src_material.Get(AI_MATKEY_COLOR_SPECULAR, aiColor))
			{
				Asset::atColor clr_specular(1.0f, aiColor.r, aiColor.g, aiColor.b);
				dst_material.set(Asset::atMaterial::CLR_SPECULAR, clr_specular);
			}
		}

		// try to load the diffuse color of material
		{
			aiColor3D aiColor;
			if (AI_SUCCESS == src_material.Get(AI_MATKEY_COLOR_AMBIENT, aiColor))
			{
				Asset::atColor clr_ambient(1.0f, aiColor.r, aiColor.g, aiColor.b);
				dst_material.set(Asset::atMaterial::CLR_AMBIENT, clr_ambient);
			}
		}

		// try to load the diffuse color of material
		{
			aiColor3D aiColor;
			if (AI_SUCCESS == src_material.Get(AI_MATKEY_COLOR_EMISSIVE, aiColor))
			{
				Asset::atColor clr_diffuse(1.0f, aiColor.r, aiColor.g, aiColor.b);
				dst_material.set(Asset::atMaterial::CLR_EMISSIVE, clr_diffuse);
			}
		}
	}

	void load_assimp_material(Asset::atMaterial& dst_material, const aiMaterial& src_material)
	{
		// try to load the name of material
		{
			std::string name;
			aiString aiName;
			if (AI_SUCCESS == src_material.Get(AI_MATKEY_NAME, aiName))
			{
				name = aiName.C_Str();
				dst_material.set(Asset::atMaterial::KEY_NAME, name);
			}
		}
		
		// try to load the shading model of material
		{
			int ai_shading_model;
			std::string shading_model = "unknown";
			if (AI_SUCCESS == src_material.Get(AI_MATKEY_SHADING_MODEL, ai_shading_model))
			{
				switch (ai_shading_model)
				{
				case aiShadingMode_Phong:
					shading_model = "phong";
					dst_material.set(Asset::atMaterial::SHADING_MODEL, shading_model);

					load_assimp_mtrl_phong(dst_material, src_material);
					break;

				default:
					dst_material.set(Asset::atMaterial::SHADING_MODEL, shading_model);
				}
			}
		}
	}


	void save_tree(nlohmann::json& root, Asset::atNode const* p_root)
	{
		using json = nlohmann::json;

		for (auto& mesh_idx : p_root->m_meshes)
		{
			root["mesh_indices"].push_back(mesh_idx);
		}

		for (auto& light_idx : p_root->m_lights)
		{
			root["light_indices"].push_back(light_idx);
		}

		json j_transform;
		save_transform(j_transform, p_root->m_transform);
		root["transform"] = j_transform;

		for (auto& child : p_root->m_children)
		{
			json j_child;
			save_tree(j_child, child);
			root["children"].push_back(j_child);
		}
	}

	void save_transform(nlohmann::json& j_transform, const Asset::atTransform& transform)
	{
		using json = nlohmann::json;

		json j_matrix;
		for (int n = 0; n < 4; ++n)
		{
			for (int m = 0; m < 4; ++m)
			{
				j_matrix.push_back(transform.m_matrix.data[m][n]);
			}
		}
		j_transform["matrix"] = j_matrix;
	}

	void save_mtrl_phong(nlohmann::json& j_material, const Asset::atMaterial& material)
	{
		using json = nlohmann::json;
		using namespace Asset;

		atMaterial& i_material = const_cast<atMaterial&>(material);

		{
			const Asset::atColor* p_clr = i_material.get<Asset::atColor>(Asset::atMaterial::CLR_DIFFUSE);
			if (nullptr != p_clr)
				save_color(j_material["clr_diffuse"], *p_clr);
		}

		{
			const Asset::atColor* p_clr = i_material.get<Asset::atColor>(Asset::atMaterial::CLR_SPECULAR);
			if (nullptr != p_clr)
				save_color(j_material["clr_specular"], *p_clr);
		}

		{
			const Asset::atColor* p_clr = i_material.get<Asset::atColor>(Asset::atMaterial::CLR_AMBIENT);
			if (nullptr != p_clr)
				save_color(j_material["clr_ambient"], *p_clr);
		}

		{
			const Asset::atColor* p_clr = i_material.get<Asset::atColor>(Asset::atMaterial::CLR_EMISSIVE);
			if (nullptr != p_clr)
				save_color(j_material["clr_emissive"], *p_clr);
		}
	}

	void save_material(nlohmann::json& j_material, const Asset::atMaterial& material)
	{
		using json = nlohmann::json;
		using namespace Asset;

		atMaterial& i_material = const_cast<atMaterial&>(material);

		{
			const std::string* p_name = i_material.get<std::string>(Asset::atMaterial::KEY_NAME);
			if (nullptr != p_name)
				j_material["name"] = *p_name;
		}

		const std::string* p_sd = i_material.get<std::string>(Asset::atMaterial::SHADING_MODEL);
		if (*p_sd == "phong")
		{
			j_material["shading_model"] = (std::string)"phong";
			save_mtrl_phong(j_material, material);
		}
	}

	void save_color(nlohmann::json& j_color, const Asset::atColor& color)
	{
		j_color.push_back(color.a);
		j_color.push_back(color.r);
		j_color.push_back(color.g);
		j_color.push_back(color.b);
	}
}

Asset::Importer::Importer() :
	m_p_scene(nullptr)
{}

Asset::Importer::~Importer()
{
	if (nullptr != m_p_scene)
		delete m_p_scene;
}

const Asset::atScene* Asset::Importer::ImportScene(const std::string& filename)
{
	Assimp::Importer	ai_importer;
	const aiScene* ai_scene = ai_importer.ReadFile(filename, impl::ai_default_ppsteps);
	if (nullptr == ai_scene)
	{
		throw std::exception("Cannot load models by using open asset import library.");
	}

	// prepare the buffer
	if (nullptr != m_p_scene)
		delete m_p_scene;
	m_p_scene = new atScene;

	try
	{
		// TODO: load materials
		if (ai_scene->HasMaterials())
		{
			for (size_t idx = 0; idx < ai_scene->mNumMaterials; ++idx)
			{
				atMaterial* p_tmp_material = new atMaterial;
				aiMaterial* p_ai_mat = ai_scene->mMaterials[idx];
				impl::load_assimp_material(*p_tmp_material, *p_ai_mat);
				m_p_scene->manage_material(p_tmp_material);
			}
		}

		// try to load meshes.
		if (ai_scene->HasMeshes())
		{
			for (size_t idx = 0; idx < ai_scene->mNumMeshes; ++idx)
			{
				atMesh* p_tmp_mesh = new atMesh;
				impl::load_assimp_mesh(*p_tmp_mesh, *(ai_scene->mMeshes[idx]));
				m_p_scene->manage_mesh(p_tmp_mesh);
			}
		}

		// try to load lights.
		if (ai_scene->HasLights())
		{
			for (size_t idx = 0; idx < ai_scene->mNumLights; ++idx)
			{
				atLight* p_tmp_light = new atLight;
				impl::load_assimp_light(*p_tmp_light, *(ai_scene->mLights[idx]));
				m_p_scene->manage_light(p_tmp_light);
			}
		}

		// try to load scene-tree
		aiNode* p_ai_root = ai_scene->mRootNode;
		atNode* p_scene_root = nullptr;
		if (nullptr != p_ai_root)
		{
			 p_scene_root = load_tree(p_ai_root);
		}
		m_p_scene->manage_scene_tree(p_scene_root);
	}
	catch (std::exception ex)
	{
		if (nullptr != m_p_scene)
		{
			delete m_p_scene;
			m_p_scene = nullptr;
		}

		throw ex;
	}

	return m_p_scene;
}

void Asset::Importer::SaveScene(const std::string& filename) const
{
	using json = nlohmann::json;

	try
	{
		json j_root;

		// output the global scene elements
				
		// output the materials
		for (auto& p_material : m_p_scene->materials())
		{
			json j_material;
			impl::save_material(j_material, *p_material);
			j_root["materials"].push_back(j_material);
		}

		// output the lights
		for (auto& light : m_p_scene->lights())
		{
			json j_light;

			j_light["name"] = light->m_name;
			j_light["type"] = atLight::_type_to_string(light->m_type);

			j_light["color_ambient"].push_back(light->m_ambient.a);
			j_light["color_ambient"].push_back(light->m_ambient.r);
			j_light["color_ambient"].push_back(light->m_ambient.g);
			j_light["color_ambient"].push_back(light->m_ambient.b);

			j_light["color_specular"].push_back(light->m_specular.a);
			j_light["color_specular"].push_back(light->m_specular.r);
			j_light["color_specular"].push_back(light->m_specular.g);
			j_light["color_specular"].push_back(light->m_specular.b);

			j_light["color_diffuse"].push_back(light->m_diffuse.a);
			j_light["color_diffuse"].push_back(light->m_diffuse.r);
			j_light["color_diffuse"].push_back(light->m_diffuse.g);
			j_light["color_diffuse"].push_back(light->m_diffuse.b);

			j_light["angle_inner_cone"] = light->m_angle_inner_cone;
			j_light["angle_outer_cone"] = light->m_angle_outer_cone;

			j_light["att_constant"] = light->m_attenuation_constant;
			j_light["att_linear"] = light->m_attenuation_linear;
			j_light["att_quadratic"] = light->m_attenuation_quadratic;

			j_light["pos"].push_back(light->m_position.x);
			j_light["pos"].push_back(light->m_position.y);
			j_light["pos"].push_back(light->m_position.z);

			j_light["dir"].push_back(light->m_direction.x);
			j_light["dir"].push_back(light->m_direction.y);
			j_light["dir"].push_back(light->m_direction.z);

			j_light["up"].push_back(light->m_up.x);
			j_light["up"].push_back(light->m_up.y);
			j_light["up"].push_back(light->m_up.z);

			j_root["lights"].push_back(j_light);
		}

		// output the meshes		
		for (auto& mesh : m_p_scene->meshes())
		{
			json j_mesh;
			for (auto& vtx : mesh->m_vertices)
			{
				json j_vtx;

				j_vtx.push_back(vtx.x);
				j_vtx.push_back(vtx.y);
				j_vtx.push_back(vtx.z);

				j_mesh["vertices"].push_back(j_vtx);
			}

			for (auto& nml : mesh->m_normals)
			{
				json j_nml;

				j_nml.push_back(nml.x);
				j_nml.push_back(nml.y);
				j_nml.push_back(nml.z);

				j_mesh["normals"].push_back(j_nml);
			}

			for (auto& face : mesh->m_faces)
			{
				json j_face;

				j_face.push_back(face.v1);
				j_face.push_back(face.v2);
				j_face.push_back(face.v3);

				j_mesh["faces"].push_back(j_face);
			}

			j_mesh["material"] = mesh->m_material_index;
			
			j_root["meshes"].push_back(j_mesh);
		}

		json j_scene_tree;
		impl::save_tree(j_scene_tree, m_p_scene->scene_tree());
		j_root["scene_tree"] = j_scene_tree;

		// output to the file
		std::ofstream output(filename, std::ios::trunc);
		if (true != output.is_open())
		{
			throw std::exception("cannot create the output file.");
		}

#ifdef _DEBUG
		output << std::setw(2) << j_root;
#else
		output << j_root;
#endif // _DEBUG

		output.close();
	}
	catch (std::exception ex)
	{
		throw ex;
	}


}

Asset::atNode* Asset::Importer::load_tree(aiNode const* p_root)
{
	try
	{
		atNode* p_node = new atNode();
		if (p_root->mNumChildren > 0)
		{
			for (index i = 0; i < p_root->mNumChildren; ++i)
			{
				atNode* child = load_tree(p_root->mChildren[i]);
				if(nullptr != child)
					p_node->m_children.push_back(child);
			}
		}

		for (index i = 0; i < p_root->mNumMeshes; ++i)
		{
			p_node->m_meshes.push_back(p_root->mMeshes[i]);
		}

		index idx_light;
		if (INVALID_INDEX != (idx_light = m_p_scene->find_light(p_root->mName.C_Str())))
		{
			p_node->m_lights.push_back(idx_light);
		}

		aiMatrix4x4 mat = p_root->mTransformation;
		p_node->m_transform.m_matrix.m00 = mat.a1;
		p_node->m_transform.m_matrix.m01 = mat.a2;
		p_node->m_transform.m_matrix.m02 = mat.a3;
		p_node->m_transform.m_matrix.m03 = mat.a4;

		p_node->m_transform.m_matrix.m10 = mat.b1;
		p_node->m_transform.m_matrix.m11 = mat.b2;
		p_node->m_transform.m_matrix.m12 = mat.b3;
		p_node->m_transform.m_matrix.m13 = mat.b4;

		p_node->m_transform.m_matrix.m20 = mat.c1;
		p_node->m_transform.m_matrix.m21 = mat.c2;
		p_node->m_transform.m_matrix.m22 = mat.c3;
		p_node->m_transform.m_matrix.m23 = mat.c4;

		p_node->m_transform.m_matrix.m30 = mat.d1;
		p_node->m_transform.m_matrix.m31 = mat.d2;
		p_node->m_transform.m_matrix.m32 = mat.d3;
		p_node->m_transform.m_matrix.m33 = mat.d4;

		return p_node;
	}
	catch (std::exception ex)
	{
		throw ex;
	}
}
